/*******************************************************************************
 * This file is part of Crunch Network.
 *
 * Crunch Network is free software: you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * Crunch Network is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along with
 * Crunch Network.  If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/

package com.crunch.network;

import com.crunch.network.util.BitStreamReader;
import com.crunch.network.util.BitStreamWriter;

import java.io.IOException;
import java.nio.BufferOverflowException;
import java.nio.BufferUnderflowException;

/**
 * TODO
 * todo: add derivative mode possibly
 */
public abstract class StateElement {
	/**
	 * Constructs the state element.
	 */
	protected StateElement(NetworkTransmissionMode networkTransmissionMode) {
		reset();
		this.networkTransmissionMode = networkTransmissionMode;
	}

	/**
	 * Called by the state table to reset this element to its default state.
	 */
	void reset() {
		flags = FLAG_DIRTY;
		packetId = QUEUED_PACKET;
	}

	/**
	 * Reads element data from the stream provided. The data read should be "compatible" with the data written in the
	 * corresponding {@link StateElement#write(com.crunch.network.util.BitStreamWriter)} method.
	 *
	 * @param   reader the stream from which element data should be read.
	 * @throws  BufferUnderflowException if too little data is available for reading.
	 * @throws  IOException if an error in the data is detected.
	 */
	public abstract void read(BitStreamReader reader) throws BufferUnderflowException, IOException;

	/**
	 * Writes element data to the stream provided. The data written should be "compatible" with the data read in the
	 * corresponding {@link StateElement#read(com.crunch.network.util.BitStreamReader)} method.
	 *
	 * @param   writer the stream to which element data should be written.
	 * @throws  BufferOverflowException if too much data is available for writing.
	 */
	public abstract void write(BitStreamWriter writer) throws BufferOverflowException;

	/**
	 * Returns whether the element is dirty. An element is dirty if its value has recently been changed and the new
	 * value has not yet been sent out in a packet (or it's been detected that the packet it was sent in was dropped).
	 *
	 * @return  whether the element is dirty.
	 */
	public final boolean isDirty() {
		return (flags & FLAG_DIRTY) != 0;
	}

	/**
	 * Returns whether the element is pending. An element is pending if its value has recently been changed and the new'
	 * value has been sent out in a packet which has not yet been acknowledged or dropped.
	 *
	 * @return  whether the element is pending.
	 */
	public final boolean isPending() {
		return (flags & FLAG_PENDING) != 0;
	}

	/**
	 * Returns whether the element was just updated and contains a new value.
	 *
	 * @return  whether the element was just updated.
	 */
	public final boolean isUpdated() {
		return (flags & FLAG_UPDATED) != 0;
	}

	/**
	 * Returns the {@link NetworkTransmissionMode} setting for this state element.
	 *
	 * @return  the {@link NetworkTransmissionMode} setting for this state element.
	 */
	public final NetworkTransmissionMode getNetworkTransmissionMode() {
		return networkTransmissionMode;
	}

	/**
	 * Indicates the mode in which a state element is transmitted over the network.
	 */
	public enum NetworkTransmissionMode {
		/**
		 * The state element is only transmitted when it is dirty.
		 */
		WHEN_DIRTY,

		/**
		 * The state element is only transmitted when the state table is first initialized.
		 */
		ON_INITIALIZATION_ONLY,

		/**
		 * The state element is always transmitted.
		 */
		ALWAYS
	}

	/**
	 * Flags the element as being dirty, indicating that an update should be sent out. This method should be called by
	 * subclasses when the value changes.
	 */
	protected final void setDirty() {
		flags |= FLAG_DIRTY;
	}

	// these functions are used by NetworkWriter to keep the status of the state element updated

	/**
	 * Special value indicating that the element has been queued to be written to a packet but has not yet actually been
	 * sent. This value can be returned from {@link StateElement#getPacketId()} if the packet this element was written
	 * to has not yet been sent, or if the write attempt failed because the packet was at capacity. In the latter case,
	 * creation messages will automatically be resent as soon as a packet has sufficient space, and the packet ID will
	 * be filled in with a valid ID.
	 */
	static final int QUEUED_PACKET = -1;

	/**
	 * Returns the ID of the packet the element is currently pending in, or the special value {@link
	 * StateElement#QUEUED_PACKET}.
	 *
	 * @return the ID of the packet the element is currently pending in.
	 */
	int getPacketId() {
		return packetId;
	}

	/**
	 * Clears the element's dirty flag and flags the element as pending but does not assign a valid packet ID. After
	 * calling this, {@link StateElement#getPacketId()} will return {@link StateElement#QUEUED_PACKET}.
	 */
	void queueForPacket() {
		flags &= ~FLAG_DIRTY;
		flags |= FLAG_PENDING;
	}

	/**
	 * Sets the packet ID. This should be called when a packet the element is included in has been successfully sent.
	 *
	 * @param   packetId the ID of the packet that the element was sent in.
	 */
	void setPacketId(int packetId) {
		this.packetId = packetId;
	}

	/**
	 * Clears the pending flag.
	 */
	void clearPending() {
		flags &= ~FLAG_PENDING;
	}

	/**
	 * Sets the updated flag, indicating that a new value has arrived for this element.
	 */
	void setUpdated() {
		flags |= FLAG_UPDATED;
	}

	/**
	 * A helper function used to determine when an element should be sent in a message.
	 *
	 * @param   creationMessage whether the message in question is a state table creation message.
	 * @param   mode the mode of the element in question.
	 * @param   dirty whether the element in question is dirty.
	 * @return  whether the element should be sent.
	 */
	static boolean shouldBeSent(boolean creationMessage, NetworkTransmissionMode mode, boolean dirty) {
		if (creationMessage) {
			return true;
		} else {
			// don't send if the transmission mode is ON_INITIALIZATION_ONLY
			return mode == NetworkTransmissionMode.ALWAYS ||
					mode == NetworkTransmissionMode.WHEN_DIRTY && dirty;
		}
	}

	// list of possible flags

	// if FLAG_PENDING is set, it indicates that the value has been sent out in a packet
	private static final int FLAG_PENDING   = 0x1;

	// the value has recently been changed and not yet sent
	private static final int FLAG_DIRTY     = 0x2;

	// the value has just received an updated
	private static final int FLAG_UPDATED   = 0x4;

	// flags for this state element
	private int flags;

	// network transmission mode
	NetworkTransmissionMode networkTransmissionMode;

	// ID of the packet this element is currently pending in
	// if the element is pending but is not yet sent out, this contains the special value QUEUED_PACKET
	// this occurs if a guaranteed message is written to a packet that is full
	private int packetId;
}
